Loopar Introduction
loopar-webpage

API Reference

Complete reference for Loopar's server-side APIs.


Core Objects

ObjectDescription
looparGlobal framework object
BaseDocumentBase class for Models
BaseControllerBase class for Controllers

Import Patterns

// In Model (.js)
import { BaseDocument } from "loopar";
import loopar from "loopar";

// In Controller (-controller.js)
import { BaseController } from "loopar";
import loopar from "loopar";

// In any server file
import loopar from "loopar";

Quick Reference

Document Operations

const doc = await loopar.getDocument("Entity", "name");
const doc = await loopar.newDocument("Entity");
await doc.save();
await doc.delete();

Database Queries

const all = await loopar.db.getAll("Entity", options);
const count = await loopar.db.count("Entity", filters);
const value = await loopar.db.getValue("Entity", "name", "field");

Utilities

const user = loopar.session.user;
const config = loopar.config.get("key");
await loopar.sendEmail({ to, subject, body });

loopar (Global Object)

The main framework object available throughout the server-side code.

import loopar from "loopar";

Document Methods

loopar.getDocument(entity, name)

Retrieve an existing document by name.

const customer = await loopar.getDocument("Customer", "CUST-0001");

// Access fields
console.log(customer.email);
console.log(customer.status);

// Call model methods
const fullName = customer.getFullName();
ParameterTypeDescription
entitystringEntity name
namestringDocument name/ID

Returns: Document instance

Throws: Error if document not found


loopar.newDocument(entity, data?)

Create a new document instance.

// Create empty
const customer = await loopar.newDocument("Customer");
customer.customer_name = "John Doe";
customer.email = "john@example.com";
await customer.save();

// Create with initial data
const task = await loopar.newDocument("Task", {
  title: "New Task",
  status: "Open",
  priority: "High"
});
await task.save();
ParameterTypeDescription
entitystringEntity name
dataobjectInitial field values (optional)

Returns: New document instance (unsaved)


loopar.deleteDocument(entity, name)

Delete a document by name.

await loopar.deleteDocument("Customer", "CUST-0001");
ParameterTypeDescription
entitystringEntity name
namestringDocument name/ID

Session & User

loopar.session

Current session information.

// Current user
const user = loopar.session.user;
const userName = loopar.session.user_name;

// Check if logged in
if (loopar.session.user) {
  // User is authenticated
}

// Current site/tenant
const site = loopar.session.site;
PropertyTypeDescription
userstringCurrent user ID
user_namestringUser display name
sitestringCurrent tenant name
is_adminbooleanIs administrator

Configuration

loopar.config

Access framework configuration.

// Get config value
const dbType = loopar.config.get("db_type");
const port = loopar.config.get("port");

// Get with default
const timeout = loopar.config.get("timeout", 5000);

Utilities

loopar.sendEmail(options)

Send an email.

await loopar.sendEmail({
  to: "customer@example.com",
  subject: "Welcome!",
  body: "<h1>Welcome to our platform</h1>",
  // Optional
  from: "noreply@yoursite.com",
  cc: ["manager@yoursite.com"],
  attachments: [{ filename: "doc.pdf", path: "/path/to/doc.pdf" }]
});
OptionTypeDescription
tostring/arrayRecipient(s)
subjectstringEmail subject
bodystringHTML body
fromstringSender (optional)
ccarrayCC recipients
bccarrayBCC recipients
attachmentsarrayFile attachments

loopar.throw(message, httpCode?)

Throw an error with optional HTTP status code.

if (!customer) {
  loopar.throw("Customer not found", 404);
}

if (!hasPermission) {
  loopar.throw("Access denied", 403);
}

loopar.log(message, level?)

Log a message.

loopar.log("Processing started");
loopar.log("Warning: slow query", "warn");
loopar.log("Error occurred", "error");
LevelDescription
infoInformation (default)
warnWarning
errorError
debugDebug (dev only)

loopar.db (Database API)

Direct database operations for querying and manipulating data.

import loopar from "loopar";

const results = await loopar.db.getAll("Customer");

Query Methods

loopar.db.getAll(entity, options?)

Get multiple documents with filtering, sorting, and pagination.

// Simple: get all
const customers = await loopar.db.getAll("Customer");

// With filters
const activeCustomers = await loopar.db.getAll("Customer", {
  filters: { status: "Active" }
});

// Complex query
const results = await loopar.db.getAll("Task", {
  filters: {
    status: ["Open", "In Progress"],  // OR condition
    priority: "High",
    project: "PROJECT-001"
  },
  fields: ["name", "title", "status", "due_date"],
  orderBy: "due_date ASC",
  limit: 20,
  offset: 0
});
OptionTypeDescription
filtersobjectField conditions
fieldsarrayFields to return
orderBystringSort order (field ASC/DESC)
limitnumberMax records to return
offsetnumberRecords to skip

Filter Operators:

// Exact match
{ status: "Active" }

// OR condition (array)
{ status: ["Open", "In Progress"] }

// Multiple conditions (AND)
{ status: "Active", priority: "High" }

// With operators
{ amount: [">", 1000] }
{ created_at: [">=", "2024-01-01"] }
{ name: ["like", "%john%"] }

loopar.db.getOne(entity, filters)

Get a single document matching filters.

const customer = await loopar.db.getOne("Customer", {
  email: "john@example.com"
});

if (customer) {
  console.log(customer.name);
}

Returns: Single document object or null


loopar.db.getValue(entity, name, field)

Get a single field value from a document.

const email = await loopar.db.getValue("Customer", "CUST-0001", "email");
const status = await loopar.db.getValue("Task", "TASK-0001", "status");
ParameterTypeDescription
entitystringEntity name
namestringDocument name
fieldstringField to retrieve

Returns: Field value or null


loopar.db.count(entity, filters?)

Count documents matching filters.

// Count all
const totalCustomers = await loopar.db.count("Customer");

// Count with filter
const activeCount = await loopar.db.count("Customer", {
  status: "Active"
});

// Count with multiple conditions
const urgentOpen = await loopar.db.count("Task", {
  status: "Open",
  priority: "Urgent"
});

Returns: Number


loopar.db.exists(entity, name)

Check if a document exists.

const exists = await loopar.db.exists("Customer", "CUST-0001");

if (!exists) {
  // Create new customer
}

Returns: Boolean


Modify Methods

loopar.db.setValue(entity, name, field, value)

Update a single field value.

await loopar.db.setValue("Customer", "CUST-0001", "status", "VIP");
await loopar.db.setValue("Task", "TASK-0001", "completed_at", new Date());

Note: This bypasses model hooks. Use doc.save() for full lifecycle.


loopar.db.setValues(entity, name, values)

Update multiple field values.

await loopar.db.setValues("Customer", "CUST-0001", {
  status: "VIP",
  credit_limit: 50000,
  modified_at: new Date()
});

Raw SQL

loopar.db.execute(sql, params?)

Execute raw SQL query.

// Simple query
const results = await loopar.db.execute(
  "SELECT * FROM Customer WHERE status = ?",
  ["Active"]
);

// Complex aggregation
const stats = await loopar.db.execute(`
  SELECT 
    project,
    COUNT(*) as task_count,
    SUM(CASE WHEN status = 'Completed' THEN 1 ELSE 0 END) as completed
  FROM Task
  WHERE created_at > ?
  GROUP BY project
`, ["2024-01-01"]);

Warning: Use parameterized queries to prevent SQL injection.


Transactions

loopar.db.transaction(callback)

Execute operations in a transaction.

await loopar.db.transaction(async (trx) => {
  // All operations use same transaction
  const order = await loopar.newDocument("Order");
  order.customer = "CUST-0001";
  order.total = 1500;
  await order.save({ trx });

  // Update customer balance
  const customer = await loopar.getDocument("Customer", "CUST-0001");
  customer.balance += 1500;
  await customer.save({ trx });

  // If any operation fails, all are rolled back
});

BaseDocument

Base class for all Models. Extend this to add custom logic to your entities.

// customer.js (MODEL)
import { BaseDocument } from "loopar";

export default class Customer extends BaseDocument {
  constructor(props) {
    super(props);
  }
}

Instance Properties

Field Values

Access and set field values directly.

// Read fields
const name = this.customer_name;
const email = this.email;
const status = this.status;

// Set fields
this.status = "Active";
this.modified_at = new Date();
this.total = this.subtotal + this.tax;

this.name

The document's unique identifier.

console.log(this.name); // "CUST-0001"

this.isNew

Whether this is a new (unsaved) document.

async beforeSave() {
  if (this.isNew) {
    this.created_at = new Date();
    this.created_by = this.session.user;
  }
}

this.session

Current session information.

const currentUser = this.session.user;
const currentSite = this.session.site;

this.request

HTTP request object (when triggered via API).

const body = this.request?.body;
const query = this.request?.query;
const headers = this.request?.headers;

Instance Methods

this.save(options?)

Save the document to database.

// Simple save
await this.save();

// Save with options
await this.save({
  ignore_permissions: true,  // Skip permission check
  ignore_validate: false,    // Skip validation
  trx: transaction           // Use transaction
});

Triggers: validate()beforeSave()beforeInsert()/beforeUpdate() → DB → afterInsert()/afterUpdate()afterSave()


this.delete()

Delete the document.

await this.delete();

Triggers: beforeDelete() → DB → afterDelete()


this.reload()

Reload document from database.

await this.reload();

this.getData()

Get document as plain object.

const data = this.getData();
// { name: "CUST-0001", customer_name: "John", email: "john@...", ... }

Lifecycle Hooks

Override these methods to add custom logic.

async validate()

Validate before any save operation.

async validate() {
  if (!this.email) {
    throw new Error("Email is required");
  }
  
  if (this.amount < 0) {
    throw new Error("Amount cannot be negative");
  }

  // Check for duplicates
  const existing = await loopar.db.getOne("Customer", {
    email: this.email,
    name: ["!=", this.name]  // Exclude self
  });
  
  if (existing) {
    throw new Error("Email already exists");
  }
}

async beforeInsert()

Before saving a NEW document.

async beforeInsert() {
  this.created_at = new Date();
  this.created_by = this.session.user;
  this.status = this.status || "Draft";
  
  // Generate custom ID
  this.customer_id = await this.generateCustomerId();
}

async afterInsert()

After saving a NEW document.

async afterInsert() {
  // Send welcome email
  await loopar.sendEmail({
    to: this.email,
    subject: "Welcome!",
    body: `Hello ${this.customer_name}!`
  });
  
  // Create related records
  const settings = await loopar.newDocument("Customer Settings");
  settings.customer = this.name;
  await settings.save();
}

async beforeUpdate()

Before updating an EXISTING document.

async beforeUpdate() {
  this.modified_at = new Date();
  this.modified_by = this.session.user;
}

async afterUpdate()

After updating an EXISTING document.

async afterUpdate() {
  // Log changes
  await this.logChanges();
  
  // Notify if status changed
  if (this.status !== this._previousStatus) {
    await this.notifyStatusChange();
  }
}

async beforeSave()

Before any save (insert OR update).

async beforeSave() {
  // Calculate totals
  this.total = this.subtotal + this.tax - this.discount;
  
  // Update full name
  this.full_name = `${this.first_name} ${this.last_name}`;
}

async afterSave()

After any save (insert OR update).

async afterSave() {
  // Update related records
  await this.updateRelatedRecords();
  
  // Clear cache
  await this.clearCache();
}

async beforeDelete()

Before deleting.

async beforeDelete() {
  // Prevent deletion of locked records
  if (this.is_locked) {
    throw new Error("Cannot delete locked record");
  }
  
  // Check for dependencies
  const orders = await loopar.db.count("Order", { customer: this.name });
  if (orders > 0) {
    throw new Error("Cannot delete customer with orders");
  }
}

async afterDelete()

After deleting.

async afterDelete() {
  // Clean up related data
  await loopar.db.execute(
    "DELETE FROM CustomerSettings WHERE customer = ?",
    [this.name]
  );
  
  // Log deletion
  console.log(`Customer ${this.name} deleted`);
}

BaseController

Base class for Controllers. Only methods prefixed with action are accessible via URL.

// customer-controller.js (CONTROLLER)
import { BaseController } from "loopar";

export default class CustomerController extends BaseController {
  
  // GET/POST /api/Customer/stats
  async actionStats() {
    return { total: 100 };
  }
}

URL Routing

Method NameURLHTTP Methods
actionView/api/Entity/viewGET, POST
actionCreate/api/Entity/createPOST
actionStats/api/Entity/statsGET, POST
actionSendEmail/api/Entity/send-emailGET, POST
privateMethodNOT accessible

Naming: actionMyAction/api/Entity/my-action (camelCase to kebab-case)


Instance Properties

this.data

Request data (body + query params).

async actionCreate() {
  const { name, email, status } = this.data;
  
  const customer = await loopar.newDocument("Customer", {
    customer_name: name,
    email,
    status
  });
  await customer.save();
  
  return { success: true, name: customer.name };
}

this.request

Full HTTP request object.

async actionUpload() {
  const file = this.request.files?.document;
  const contentType = this.request.headers["content-type"];
  const method = this.request.method;
  
  // ...
}
PropertyDescription
methodHTTP method (GET, POST, etc.)
headersRequest headers
queryQuery string params
bodyRequest body
filesUploaded files
paramsURL params

this.response

HTTP response object.

async actionDownload() {
  this.response.setHeader("Content-Type", "application/pdf");
  this.response.setHeader("Content-Disposition", "attachment; filename=report.pdf");
  
  // Return file buffer
  return fileBuffer;
}

this.session

Current session.

async actionProfile() {
  const user = this.session.user;
  
  if (!user) {
    loopar.throw("Not authenticated", 401);
  }
  
  return await loopar.getDocument("User", user);
}

Action Examples

Basic CRUD Action

// POST /api/Customer/create
async actionCreate() {
  const customer = await loopar.newDocument("Customer", this.data);
  await customer.save();
  
  return {
    success: true,
    message: "Customer created",
    name: customer.name
  };
}

Query Action

// GET /api/Customer/search?q=john&status=Active
async actionSearch() {
  const { q, status, limit = 20 } = this.data;
  
  const filters = {};
  if (status) filters.status = status;
  if (q) filters.customer_name = ["like", `%${q}%`];
  
  const customers = await loopar.db.getAll("Customer", {
    filters,
    limit: parseInt(limit),
    orderBy: "customer_name ASC"
  });
  
  return { customers };
}

Action with Document

// POST /api/Customer/upgrade-to-vip
async actionUpgradeToVip() {
  const { name } = this.data;
  
  if (!name) {
    loopar.throw("Customer name required", 400);
  }
  
  const customer = await loopar.getDocument("Customer", name);
  customer.status = "VIP";
  customer.vip_since = new Date();
  await customer.save();
  
  // Call model method
  const discount = customer.calculateVIPDiscount();
  
  return {
    success: true,
    message: `${customer.customer_name} is now VIP`,
    discount
  };
}

Stats/Aggregation Action

// GET /api/Task/dashboard
async actionDashboard() {
  const user = this.session.user;
  
  const [open, inProgress, completed, overdue] = await Promise.all([
    loopar.db.count("Task", { status: "Open", assigned_to: user }),
    loopar.db.count("Task", { status: "In Progress", assigned_to: user }),
    loopar.db.count("Task", { status: "Completed", assigned_to: user }),
    loopar.db.execute(
      `SELECT COUNT(*) as count FROM Task 
       WHERE assigned_to = ? AND due_date < NOW() AND status != 'Completed'`,
      [user]
    ).then(r => r[0]?.count || 0)
  ]);
  
  return {
    open,
    inProgress,
    completed,
    overdue,
    total: open + inProgress + completed
  };
}

File Download Action

// GET /api/Report/export?format=csv
async actionExport() {
  const { format = "csv" } = this.data;
  
  const data = await loopar.db.getAll("Customer");
  
  if (format === "csv") {
    const csv = this.convertToCSV(data);
    this.response.setHeader("Content-Type", "text/csv");
    this.response.setHeader("Content-Disposition", "attachment; filename=customers.csv");
    return csv;
  }
  
  return { data };
}

// Private helper (NOT URL accessible)
convertToCSV(data) {
  // ...
}

Error Handling

async actionRiskyOperation() {
  try {
    // Risky operation
    await this.performOperation();
    return { success: true };
    
  } catch (error) {
    // Log error
    loopar.log(error.message, "error");
    
    // Return error response
    loopar.throw(error.message, 500);
  }
}

Authentication Check

async actionSecureAction() {
  // Check authentication
  if (!this.session.user) {
    loopar.throw("Authentication required", 401);
  }
  
  // Check admin
  if (!this.session.is_admin) {
    loopar.throw("Admin access required", 403);
  }
  
  // Proceed with action
  return { secret: "data" };
}

REST API

Loopar automatically generates REST endpoints for all entities.


Auto-Generated Endpoints

MethodEndpointDescription
GET/api/{Entity}List documents
GET/api/{Entity}/{name}Get single document
POST/api/{Entity}Create document
PUT/api/{Entity}/{name}Update document
DELETE/api/{Entity}/{name}Delete document

List Documents

GET /api/Customer

Query Parameters:

ParamDescriptionExample
pagePage number?page=2
limitRecords per page?limit=50
searchSearch term?search=john
order_bySort field?order_by=created_at
orderSort direction?order=desc
fieldsFields to return?fields=name,email
{field}Filter by field?status=Active

Example:

GET /api/Customer?status=Active&limit=20&order_by=customer_name&order=asc

Response:

{
  "data": [
    { "name": "CUST-0001", "customer_name": "John", "email": "john@..." },
    { "name": "CUST-0002", "customer_name": "Jane", "email": "jane@..." }
  ],
  "total": 150,
  "page": 1,
  "limit": 20,
  "pages": 8
}

Get Single Document

GET /api/Customer/CUST-0001

Response:

{
  "name": "CUST-0001",
  "customer_name": "John Doe",
  "email": "john@example.com",
  "status": "Active",
  "created_at": "2024-01-15T10:30:00Z"
}

Create Document

POST /api/Customer
Content-Type: application/json

{
  "customer_name": "New Customer",
  "email": "new@example.com",
  "status": "Lead"
}

Response:

{
  "success": true,
  "name": "CUST-0003",
  "message": "Document created"
}

Update Document

PUT /api/Customer/CUST-0001
Content-Type: application/json

{
  "status": "VIP",
  "credit_limit": 50000
}

Response:

{
  "success": true,
  "name": "CUST-0001",
  "message": "Document updated"
}

Delete Document

DELETE /api/Customer/CUST-0001

Response:

{
  "success": true,
  "message": "Document deleted"
}

Custom Actions

Call controller actions:

# GET action
GET /api/Customer/stats

# POST action with data
POST /api/Customer/upgrade-to-vip
Content-Type: application/json

{
  "name": "CUST-0001"
}

Error Responses

{
  "error": true,
  "message": "Customer not found",
  "status": 404
}
StatusMeaning
400Bad request / Validation error
401Authentication required
403Permission denied
404Document not found
500Server error

Authentication

Include session token in requests:

GET /api/Customer
Authorization: Bearer {token}

Or use cookie-based session after login.

Client-Side API

React hooks and utilities for client-side code (.jsx files).


useDocument Hook

Access and manipulate the current document in a form.

import { useDocument } from "@loopar/components";

export default function CustomerForm(props) {
  const { 
    document,     // Current document data
    setValue,     // Set field value
    getValue,     // Get field value
    save,         // Save document
    loading,      // Loading state
    errors        // Validation errors
  } = useDocument();

  return (
    <div>
      <h1>{document.customer_name}</h1>
      <p>Status: {document.status}</p>
      
      <button 
        onClick={() => setValue("status", "VIP")}
        disabled={loading}
      >
        Upgrade to VIP
      </button>
      
      <button onClick={save} disabled={loading}>
        {loading ? "Saving..." : "Save"}
      </button>
    </div>
  );
}

setValue(field, value)

Update a field value.

setValue("status", "Active");
setValue("amount", 1500.50);
setValue("items", [...document.items, newItem]);

getValue(field)

Get current field value.

const status = getValue("status");
const total = getValue("total");

save()

Save the document.

const handleSave = async () => {
  try {
    await save();
    toast.success("Saved successfully!");
  } catch (error) {
    toast.error(error.message);
  }
};

useLoopar Hook

Access Loopar utilities.

import { useLoopar } from "@loopar/components";

export default function MyComponent() {
  const loopar = useLoopar();
  
  const handleAction = async () => {
    const result = await loopar.call({
      entity: "Customer",
      action: "stats"
    });
    
    console.log(result);
  };
  
  return <button onClick={handleAction}>Get Stats</button>;
}

loopar.call(options)

Call a server action.

// GET action
const stats = await loopar.call({
  entity: "Customer",
  action: "stats"
});

// POST action with data
const result = await loopar.call({
  entity: "Customer",
  action: "upgrade-to-vip",
  method: "POST",
  data: { name: "CUST-0001" }
});

loopar.navigate(path)

Navigate to a different page.

loopar.navigate("/desk/Customer/list");
loopar.navigate("/desk/Customer/edit/CUST-0001");

BaseForm Component

Wrapper for entity forms.

import { BaseForm } from "@loopar/components";

export default function CustomerForm(props) {
  return (
    <BaseForm {...props}>
      {/* Custom content renders above default fields */}
      <div className="custom-header">
        {/* Custom UI */}
      </div>
      
      {/* Default fields render automatically */}
    </BaseForm>
  );
}

Utility Components

import { 
  Button,
  Input,
  Select,
  Badge,
  Alert,
  Card,
  Table
} from "@loopar/components";

// Or from shadcn/ui
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

Example: Custom Form UI

import { BaseForm, useDocument } from "@loopar/components";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CheckCircle, AlertTriangle } from "lucide-react";

export default function TaskForm(props) {
  const { document, setValue, save, loading } = useDocument();

  const markComplete = async () => {
    setValue("status", "Completed");
    setValue("completed_at", new Date().toISOString());
    await save();
  };

  const isOverdue = document.due_date && 
    new Date(document.due_date) < new Date() && 
    document.status !== "Completed";

  return (
    <BaseForm {...props}>
      {/* Status Header */}
      <div className="flex items-center justify-between p-4 bg-muted rounded-lg mb-6">
        <div className="flex items-center gap-2">
          <h2 className="font-semibold">{document.title || "New Task"}</h2>
          <Badge variant={document.status === "Completed" ? "success" : "default"}>
            {document.status}
          </Badge>
        </div>
        
        {document.status !== "Completed" && (
          <Button onClick={markComplete} disabled={loading}>
            <CheckCircle className="w-4 h-4 mr-2" />
            Mark Complete
          </Button>
        )}
      </div>

      {/* Overdue Warning */}
      {isOverdue && (
        <div className="p-3 mb-4 bg-destructive/10 text-destructive rounded-lg flex items-center gap-2">
          <AlertTriangle className="w-4 h-4" />
          This task is overdue!
        </div>
      )}
    </BaseForm>
  );
}